from pyglet.event import EventDispatcher



class combineddict(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        self.bases = []

    def __getitem__(self, item):
        try:
            return dict.__getitem__(self, item)
        except KeyError:
            for base in reversed(self.bases):
                try:
                    return base[item]
                except KeyError:
                    pass
        raise KeyError(item)

    def copy(self):
        r = combineddict(self)
        r.bases = self.bases
        return r


class Undefined(object):
    def __eq__(self, other):
        return other is Undefined


class StyleAttr(object):
    __slots__ = ['group', 'converter', 'default']

    def __init__(self, group=Undefined, converter=Undefined, default=Undefined):
        self.group = group
        self.converter = converter
        self.default = default


class StyleAttrDict(dict):
    def __init__(self,
            default_group=None, 
            default_converter=None, 
            default_default=None,
            initial=None,
        ):
        self.default_group = default_group
        self.default_converter = default_converter
        self.default_default = default_default
        dict.__init__(self)
        if initial:
            self.update(initial)
        
    def __setitem__(self, item, value):
        if not isinstance(value, StyleAttr):
            value = StyleAttr(default=value)
        if value.group is Undefined:
            value.group = self.default_group
        if value.converter is Undefined:
            value.converter = self.default_converter
        if value.default is Undefined:
            value.default = self.default_default
        dict.__setitem__(self, item, value)

    def update(self, other):
        for k, v in other.iteritems():
            self.__setitem__(k, v)
        

class ReMappedStyle(dict):
    pass


class styledict(dict, EventDispatcher):
    def __init__(self, attrs, initial={}):
        self.attrdefs = attrs
        self.dirty_groups = set()
        dict.__init__(self, initial)

    def __setitem__(self, item, value, dispatch=True):
        try:
            attrdef = self.attrdefs[item]
            value = attrdef.converter(value)
            group = attrdef.group
        except KeyError:
            group = None

        if isinstance(value, ReMappedStyle):
            self.update(value)
            return           

        dict.__setitem__(self, item, value)
            
        if dispatch:
            self.dispatch_event('on_style_change', {item: value}, set([group]))
        else:
            return group

    def __getitem__(self, item):
        try:
            return self[item]
        except KeyError:
            try:
                return self.attrdefs[item].default
            except KeyError:
                pass
            raise KeyError(item)                  

    def __getattr__(self, item):
        return self.__getitem__(item)

    def __setattr__(self, item, value):
        self.__setitem__(item, value)

    def __delattr__(self, item):
        del self[item]

    def update(self, other):
        groups = set()
        for k, v in other.iteritems():
            groups.add(self.__setitem__(k, v, False))
        self.dispatch_event('on_style_change', other, groups)

    def on_style_change(self, attrs, groups):
        self.dirty_groups.update(groups)

    def recalc_dirty(self):
        for group in self.dirty_groups:
            try:
                f = getattr(self, 'recalc_%s' % group)
            except AttributeError:
                print "cant recalc group %s" % group
                continue
            f()
        self.dirty_groups.clear()

styledict.register_event_type('on_style_change')


class BaseStyle(dict, EventDispatcher):
    def __init__(self, attrdef, initial=None):
        dict.__init__(self)
        self.attrdef = attrdef

        self.generic_cache = {}

        if hasattr(self, 'default'):
            self._update(self.default)
        if initial:
            self._update(initial)

        self.push_handlers(self)

    def _create_child_cb(self, k, v):
        def dispatch_change(attrs):
            self.dispatch_event('on_style_change', {k:attrs})
        v.push_handlers(on_style_change=dispatch_change)

    def __setitem__(self, item, value):
        if not isinstance(value, styledict):
            value = styledict(self.attrdef, value)
        if self.has_key(item):
            self[item].pop_handlers()
        
        self._create_child_cb(item, value)
        styledict.__setitem__(self, item, value)

    def _update(self, other):
        for k, v in other.iteritems():
            if not isinstance(v, styledict):
                v = styledict(v)
            if self.has_key(k):
                self[k].update(v)
            else:
                self[k] = v
                self._create_child_cb(k, v)

    def update(self, other):
        self._update(other)
        self.dispatch_event('on_style_change', other)

    def on_style_change(self, attrs):
        # TODO optimise :]
        self.generic_cache.clear()

    def get_for_generic_control(self, control):
        controlname = control.__class__.__name__
        classname = control.classname
        name = control.name
        generic_cache_key = (name, classname, controlname)

        try:
            ret = self.generic_cache[generic_cache_key]
        except KeyError:
            pass

        self.generic_cache[generic_cache_key] = generic_cache = combineddict()

        # order: 
        # (control) base classes
        # (control.classname) base classes + classname
        # (.classname) classname
        # (control) current class
        # (control.classname) current class + classname
        # (#name) name

        self._apply_bases(control.__class__.__bases__, classname, generic_cache)

        if self.has_key(controlname):
            generic_cache.bases.append(self[controlname])

        if classname and self.has_key('.%s' % classname):
            generic_cache.bases.append(self['.%s' % classname])

        if classname and self.has_key('%s.%s' % (controlname, classname)):
            generic_cache.bases.append(self['%s.%s' % (controlname, classname)])

        if name and self.has_key('#%s' % name):
            generic_cache.bases.append(self['#%s' % name])

        return generic_cache

    def compute(self, control):
        if control._computed_style is not None:
            style = control._computed_style
            style.clear()
        else:
            control._computed_style = style = combineddict()
            style.bases.append(self.get_for_generic_control(control))
            style.bases.append(control.style)
        self.calculate_computed(control, style)

    def calculate_computed(self, control, style):
        raise NotImplemented()

    def _apply_bases(self, bases, classname, ret):
        for base in bases:
            base_name = base.__name__
            if classname and self.has_key('%s.%s' % (base_name, classname)):
                ret.bases.insert(0, self['%s.%s' % (base_name, classname)])
            if self.has_key(base_name):
                ret.bases.insert(0, self[base_name])
            if base.__bases__:
                self._apply_bases(base.__bases__, classname, ret)

    def ___apply_bases(self, bases, classname, ret):
        # we have to mess around to maintain order
        for base in bases:
            if base.__bases__:
                self._apply_bases(base.__bases__, classname, ret)
        for base in bases:
            base_name = base.__name__
            if self.has_key(base_name):
                ret.bases.append(self[base_name])
        for base in bases:
            base_name = base.__name__
            if classname and self.has_key('%s.%s' % (base_name, classname)):
                ret.update(self['%s.%s' % (base_name, classname)])

BaseStyle.register_event_type('on_style_change')

Style = BaseStyle

class DefaultStyle(BaseStyle):
    def __init__(self, attrdef, initial=None):
        defs = StyleAttrDict(initial=dict(
            left = StyleAttr('layout'),
            right = StyleAttr('layout'),
            top = StyleAttr('layout'),
            bottom = StyleAttr('layout'),
            width = StyleAttr('layout'),

            client_x = None,
            client_y = None,

            client_x=None,
            client_y=None,
            client_width=None,
            client_height=None,

            padding=None,

            min_width=StyleAttr('layout'),
            min_height=StyleAttr('layout'),
            max_width=StyleAttr('layout'),
            max_height=StyleAttr('layout'),

            display=None,
            overflow=None,

            cursor=None,

            layout=None,
            layout_columns=None,
            layout_rows=None,

            border_width=None,
            border_color=None,
            border_style=None,

            background_image=None,
            background_image_halign=None,
            background_image_valign=None,
            # TODO should probably have top/left/right/bottom logic for these.
            background_image_left=None,
            background_image_bottom=None,
            background_color=None,
            background_color2=None,
            background_style=None,

            color=StyleAttr('look', default=(0, 0, 0)),

            font_name=None,
            font_size=None,
            font_bold=None,
            font_italic=None,

            text_align=None,

            halign=None,
            valign=None,
        ))
        if attrdef:
            defs.update(attrdef)
        BaseStyle.__init__(self, defs, initial)

    def recalc_layout(self):
        parent = control._parent
        if parent:
            pcs = parent._computed_style
        else:
            pcs = None

        if s['width'] is None:
            if s['left'] is not None and s['right'] is not None:
                s['width'] = pcs['client_width'] - s['right'] - s['left']
            elif pcs is not None and parent._root is not parent:
                s['width'] = pcs['client_width']
            else:
                s['width'] = 100
        if s['height'] is None:
            if s['top'] is not None and s['bottom'] is not None:
                s['height'] = pcs['client_height'] - s['bottom'] - s['top']
            elif pcs is not None and parent._root is not parent:  
                s['height'] = pcs['client_height']
            else:
                s['height'] = 50

        if s['max_width'] is not None and s['width'] > s['max_width']:
            s['width'] = s['max_width']
        if s['max_height'] is not None and s['height'] > s['max_height']:
            s['height'] = s['max_height']
        if s['min_width'] is not None and s['width'] < s['min_width']:
            s['width'] = s['min_width']
        if s['min_height'] is not None and s['height'] < s['min_height']:
            s['height'] = s['min_height']

        if s['left'] is None:
            if s['right'] is not None and pcs is not None:
                s['left'] = pcs['client_width'] - s['right'] - s['width']
            elif pcs is not None:
                s['left'] = pcs['client_x']
            else:
                s['left'] = 0
        if s['bottom'] is None:
            if s['top'] is not None and pcs is not None:
                s['bottom'] = pcs['client_height'] - s['top'] - s['height']
            elif pcs is not None:
                s['bottom'] = pcs['client_y']
            else:
                s['bottom'] = 0
  
        p = s['padding'] or 0
           
        if s['client_x'] is None:
            s['client_x'] = p
        if s['client_y'] is None:
            s['client_y'] = p
        if s['client_width'] is None:
            s['client_width'] = s['width'] - (2*p)
        if s['client_height'] is None:
            s['client_height'] = s['height'] - (2*p)

        if pcs:
            s['screen_x'] = pcs['screen_x'] + s['left']
            s['screen_y'] = pcs['screen_y'] + s['bottom']
        else:
            s['screen_x'] = s['left']
            s['screen_y'] = s['bottom']

        if s['background_image'] is not None:
            if s['background_image_left'] is None:
                if s['background_image_valign'] == 'center':
                    s['background_image_left'] = ((s['width'] / 2) - 
                                                  (s['background_image'].width / 2))
                elif s['background_image_valign'] == 'right':
                    s['background_image_left'] = (s['width'] - 
                                                  s['background_image'].height)
                else:
                    s['background_image_left'] = 0
            if s['background_image_bottom'] is None:
                if s['background_image_valign'] == 'center':
                    s['background_image_bottom'] = ((s['height'] / 2) - 
                                                 (s['background_image'].height / 2))
                elif s['background_image_valign'] == 'right':
                    s['background_image_bottom'] = (s['height'] - 
                                                 s['background_image'].height)
                else:
                    s['background_image_bottom'] = 0
  


class DefaultStyle(BaseStyle):
    attrdef = {'Control': dict(
        left=None,
        right=None,
        top=None,
        bottom=None,
        width=None,
        height=None,

        client_x=None,
        client_y=None,
        client_width=None,
        client_height=None,

        padding=None,

        min_width=None,
        min_height=None,
        max_width=None,
        max_height=None,

        display=None,
        overflow=None,
    
        cursor=None,

        layout=None,
        layout_columns=None,
        layout_rows=None,

        border_width=None,
        border_color=None,
        border_style=None,

        background_image=None,
        background_image_halign=None,
        background_image_valign=None,
        # TODO should probably have top/left/right/bottom logic for these.
        background_image_left=None,
        background_image_bottom=None,
        background_color=None,
        background_color2=None,
        background_style=None,

        color=(0, 0, 0),

        font_name=None,
        font_size=None,
        font_bold=None,
        font_italic=None,
    
        text_align=None,

        halign=None,
        valign=None,
    )}

    def calculate_computed(self, control, s):
        self._calculate_positional(control, s)
        if s['layout'] is not None:
            self._calculate_layout(control, s)

    def _calculate_positional(self, control, s):
        parent = control._parent
        if parent:
            pcs = parent._computed_style
        else:
            pcs = None

        if s['width'] is None:
            if s['left'] is not None and s['right'] is not None:
                s['width'] = pcs['client_width'] - s['right'] - s['left']
            elif pcs is not None and parent._root is not parent:
                s['width'] = pcs['client_width']
            else:
                s['width'] = 100
        if s['height'] is None:
            if s['top'] is not None and s['bottom'] is not None:
                s['height'] = pcs['client_height'] - s['bottom'] - s['top']
            elif pcs is not None and parent._root is not parent:  
                s['height'] = pcs['client_height']
            else:
                s['height'] = 50

        if s['max_width'] is not None and s['width'] > s['max_width']:
            s['width'] = s['max_width']
        if s['max_height'] is not None and s['height'] > s['max_height']:
            s['height'] = s['max_height']
        if s['min_width'] is not None and s['width'] < s['min_width']:
            s['width'] = s['min_width']
        if s['min_height'] is not None and s['height'] < s['min_height']:
            s['height'] = s['min_height']

        if s['left'] is None:
            if s['right'] is not None and pcs is not None:
                s['left'] = pcs['client_width'] - s['right'] - s['width']
            elif pcs is not None:
                s['left'] = pcs['client_x']
            else:
                s['left'] = 0
        if s['bottom'] is None:
            if s['top'] is not None and pcs is not None:
                s['bottom'] = pcs['client_height'] - s['top'] - s['height']
            elif pcs is not None:
                s['bottom'] = pcs['client_y']
            else:
                s['bottom'] = 0
  
        p = s['padding'] or 0
           
        if s['client_x'] is None:
            s['client_x'] = p
        if s['client_y'] is None:
            s['client_y'] = p
        if s['client_width'] is None:
            s['client_width'] = s['width'] - (2*p)
        if s['client_height'] is None:
            s['client_height'] = s['height'] - (2*p)

        if pcs:
            s['screen_x'] = pcs['screen_x'] + s['left']
            s['screen_y'] = pcs['screen_y'] + s['bottom']
        else:
            s['screen_x'] = s['left']
            s['screen_y'] = s['bottom']

        if s['background_image'] is not None:
            if s['background_image_left'] is None:
                if s['background_image_valign'] == 'center':
                    s['background_image_left'] = ((s['width'] / 2) - 
                                                  (s['background_image'].width / 2))
                elif s['background_image_valign'] == 'right':
                    s['background_image_left'] = (s['width'] - 
                                                  s['background_image'].height)
                else:
                    s['background_image_left'] = 0
            if s['background_image_bottom'] is None:
                if s['background_image_valign'] == 'center':
                    s['background_image_bottom'] = ((s['height'] / 2) - 
                                                 (s['background_image'].height / 2))
                elif s['background_image_valign'] == 'right':
                    s['background_image_bottom'] = (s['height'] - 
                                                 s['background_image'].height)
                else:
                    s['background_image_bottom'] = 0
                
    def _calculate_layout(self, control, s):
        if s['layout'] == 'strict_grid':
            rows = len(control._children) / s['layout_columns']
            
            if s['layout_rows'] is not None:
                rows = min(rows, s['layout_rows'])
            if rows == 0:
                rows = 1

            child_iter = iter(control._children)

            child_width = s['client_width'] / s['layout_columns']
            child_height = s['client_height'] / rows

            for ix in range(s['layout_columns']):
                for iy in range(rows):
                    try:
                        child = child_iter.next()
                    except StopIteration:
                        return
                    child.style['left'] = ix * child_width
                    child.style['top'] = iy * child_height
                    child.style['right'] = child.style['bottom'] = None
                    child.style['width'] = child_width
                    child.style['height'] = child_height
        else:
            s['layout']()
